-
Notifications
You must be signed in to change notification settings - Fork 14.8k
[draft] Dialect Conversion without Rollback #93412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[draft] Dialect Conversion without Rollback #93412
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, this inherits from ConversionPatternRewriter
to ensure that the conversion patterns can be used with the new OneShotConversionPatternRewriter
. While this makes sense from a functional perspective, the introduced inheritance relation seems a bit conflicting, as the super class has supposedly more functionality than the derived class. In many practical cases, replacing usages of ConversionPatternRewriter
with a OneShotCovnersionPatternRewriter
might work, this is not true in the general case, right?
The final
These functions would have to become virtual, so that they can be overridden in |
I was mainly referring to the rollback logic. A pattern using the super class might assume rollback, which then breaks when it uses the specialised class. This violates the liskov substitution principle, which might be considered problematic. I just fear that we will never be able to remove the inheritance if we build this in now. Note: From this standpoint, swapping their position in the inheritance hierarchy might be cleaner (just theoretically). |
You are right, there is one thing that conversion patterns are no longer allowed to do. From the RFC:
We already have assertions that check this in the greedy pattern rewrite driver ( Swapping the order of the two conversion rewriters in the class hierarchy won't make a difference here: for patterns to work with the new driver, they must not make changes to the IR and then
I share some of that concern. But I find it important to build something that is backwards compatible (with existing conversion patterns); with a clear and simple migration path. Otherwise, nobody is going to use the new driver. (There are so many helpers like function op conversion patterns or structural SCF conversion patterns, that people will want to reuse.) |
return impl->currentTypeConverter; | ||
} | ||
|
||
LogicalResult ConversionPatternRewriter::getAdapterOperands( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we agree on Adapter/Adaptor spelling? E.g., we already have OpAdaptor
.
const FrozenRewritePatternSet &patterns, | ||
const GreedyRewriteConfig &config); | ||
|
||
/// Add the given operation to the worklist. | ||
void addSingleOpToWorklist(Operation *op); | ||
|
||
/// Add the given operation and its ancestors to the worklist. | ||
void addToWorklist(Operation *op); | ||
virtual void addToWorklist(Operation *op); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have an estimate of how much overhead adding a vtable to this class introduces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can avoid this virtual function call by templatizing the driver class.
void startOpModification(Operation *op) override { | ||
PatternRewriter::startOpModification(op); | ||
} | ||
|
||
void finalizeOpModification(Operation *op) override { | ||
PatternRewriter::finalizeOpModification(op); | ||
} | ||
|
||
void cancelOpModification(Operation *op) override { | ||
PatternRewriter::cancelOpModification(op); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would these still be necessary after the old driver is removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only one that would still be necessary is replaceOp
because it must insert unrealized_conversion_casts. All the others could be removed (because the intermediate ConversionPatternRewriter
class would be gone).
|
||
/// A cache for unrealized_conversion_casts. To ensure that identical casts | ||
/// are not built multiple times. | ||
DenseMap<std::pair<Value, Type>, Value> castCache; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, is it possible that the same original value is casted to multiple different types within the same conversion? Type converter is currently unaware of the surrounding context, so it's unclear to me how that could happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can happen during getAdapterOperands
. Each conversion pattern can have its own type converter. (That's why there is currentTypeConverter
etc.)
func.func @foo(%arg0: i1) {
"op1"(%arg0)
"op2"(%arg0)
}
Let's assume that there is no conversion pattern for func.func
. I.e., the block argument is not converted. We could have two different patterns for "op1" and "op2" with different type converters that convert i1
to different types. Now we have to insert two unrealized_conversion_casts with different result types.
(This probably does not happen very often.)
return castOp.getOutputs()[0]; | ||
} | ||
|
||
class ConversionPatternRewriteDriver : public GreedyPatternRewriteDriver { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relying on the greedy driver makes me concerned about the "one shot" aspect.
I would really like to see a simple implementation, with the minimum amount of bookkeeping (ideally not even a worklist!), and relying on the GreedyPatternRewriteDriver does not really go in this direction right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"One Shot" may be a misnomer. I really meant "no rollback".
I am reluctant to build something that is not compatible with the large number of existing conversion patterns. I would rather gradually improve the situation in way where users have a painless way to migrate to the new driver. Otherwise, is anybody going to use the new infrastructure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the main issue that you see with a worklist-based approach? Imo, the main complexity of the dialect conversion is because of rollback and late materialization. I never had issues issues debugging a greedy pattern rewrite with -debug
so far.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have problem with dialect conversion complexity actually, other than replaceAllUsesWith not doing what it says it does, and the fact that you have to go through the rewriter for these. But you're not changing these aspects are you?
I am reluctant to build something that is not compatible with the large number of existing conversion patterns.
Without rollback you're incompatible anyway, people can't "just adopt it". But otherwise what kind of incompatibility are you worried about here?
Greedy is a fixed-point iterative algorithm which has non-trivial cost associated with it. I don't quite get why it is suitable here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have problem with dialect conversion complexity actually, other than replaceAllUsesWith not doing what it says it does
I tried to fix that here. It sounded like the rollback logic was getting too complex. (I would agree with that.)
Without rollback you're incompatible anyway, people can't "just adopt it".
I expect that most dialect conversions do not actually need the rollback. I am not aware of any passes in MLIR that require it. (But users may have their own passes that rely on it, that's why I asked in the RFC.) So my hope is that the existing patterns just work with the new driver. I only tried it with NVGPUToNVVM.cpp
and ComplexToStandard.cpp
so far, and have to try migrating a few more passes to get a better picture.
But you are right, it is not a general replacement. That's why I would keep both drivers side-by-side for a while, so that there is time to handle the tricky cases (if any). (And patterns can be updated gradually. And they can be updated in such a way that they stay compatible with both drivers.)
Greedy is a fixed-point iterative algorithm which has non-trivial cost associated with it. I don't quite get why it is suitable here?
Part of the answer is that I wanted to build something that is compatible. And that requires us to maintain some sort of worklist or resort to (unbounded) recursion, as the current implementation does.
There are some differences to a greedy fixed-point rewrite. (The following are properties of the current and the "new" dialect conversion.)
- Ops are always processed from top to bottom.
- Patterns are applied only to illegal root ops.
- A pattern must either remove the illegal root op or in-place modify it. (We may be able to tighten the rules to require that an in-place modification must make the op legal. That would gives us a stronger guarantee about the worst-case number of pattern applications.)
New illegal ops are allowed to be created, that's why the worklist is needed. The second property is an important one: we do not put an op onto the worklist just because something changed in the vicinity of the op (like in a greedy pattern rewrite).
So I wouldn't call it a greedy rewrite. Jacques asked how this is different from equipping the greedy pattern rewrite driver with a legality function. That got me thinking: large parts of the greedy driver can be reused, only the condition when to put ops on the worklist is different. The only reason why this implementation is in GreedyPatternRewriteDriver.cpp
is to safe a few lines code. (And I would eventually rename this file to WorklistBasedPatternDrivers.cpp
etc.)
This commit adds a dialect conversion driver without rollback: `OneShotDialectConversionDriver` The new driver reuses some functionality of the greedy pattern rewrite driver. Just a proof of concept, code is not polished yet. `OneShotConversionPatternRewriter` is a rewriter that materializes all IR changes immediately.
11dfecd
to
6c04a06
Compare
Subsumed by #151865. |
This commit adds a dialect conversion driver without rollback:
OneShotDialectConversionDriver
The new driver reuses some functionality of the greedy pattern rewrite driver. Just a proof of concept, code is not polished yet.
OneShotConversionPatternRewriter
is a rewriter that materializes all IR changes immediately.Adapted two tests to show what kind of changes are needed:
nvgpu-to-nvvm
andcomplex-to-standard